﻿namespace Sustainalytics.LongRunningTaskManager.Service
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Claims;
    using System.Threading.Tasks;
    using System.Web.Http;

    using Microsoft.Owin.Security;
    using Newtonsoft.Json;
    using Sustainalytics.Auth.Common;
    using Sustainalytics.AwsS3.Client;
    using Sustainalytics.FileManager;
    using Sustainalytics.LongRunningTaskManager.Interface;
    using Sustainalytics.Utils.Configuration;
    using System.Web.Http.Cors;
    using System.Diagnostics;

    [EnableCors(origins: "*", headers: "*", methods: "*")]
    [RoutePrefix("api/v1/task")]
    public class LongRunningTaskController : ApiController
    {
        // streaming buffer size
        private const int BufferSize = 100000;

        private readonly LongRunningTaskManagerHostConfiguration _config;

        // file manager
        private readonly IFileManager _fileManager;

        // task manager
        private readonly ILongRunningTaskManager _taskManager;

        private EventLog _eventLog;
        private PerformanceCounter _counter;

        /// <summary>
        /// Initializes a new instance of the <see cref="LongRunningTaskController"/> class. 
        /// </summary>
        public LongRunningTaskController()
        {
            _config = LongRunningTaskManagerHostConfiguration.Current;

            // create task manager
            _taskManager = LongRunningTaskHubManager.Instance.TaskManager;

            // get task manager settings
            var taskManagerSettings = LongRunningTaskManagerSettings.FromConfig();

            // create file manager
            _fileManager = new DiskFileManager(this._config.DiskFileManagerBaseDirectory);

            this._eventLog = new EventLog("Application");
            this._eventLog.Source = "LongRunningTaskController";
            this._counter = new PerformanceCounter("Process", "% Processor Time", "Sustainalytics.Portfolios.Service", true);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="LongRunningTaskController"/> class. 
        /// </summary>
        /// <param name="taskManager"> Task manager. </param>
        /// <param name="fileManager">File manager</param>
        /// <param name="config">config options</param>
        public LongRunningTaskController(
            ILongRunningTaskManager taskManager, 
            IFileManager fileManager, 
            LongRunningTaskManagerHostConfiguration config)
        {
            _config = config;
            _taskManager = taskManager;
            _fileManager = fileManager;
            this._eventLog = new EventLog("Application");
            this._eventLog.Source = "LongRunningTaskController";
            this._counter = new PerformanceCounter("Process", "% Processor Time", "Sustainalytics.Portfolios.Service", true);
        }

        /// <summary>
        /// Gets Task manager.
        /// </summary>
        public ILongRunningTaskManager TaskManager
        {
            get
            {
                return _taskManager;
            }
        }

        [Route("{taskInstanceId}")]
        public async Task<HttpResponseMessage> Delete(string taskInstanceId)
        {
            
            // get authentication ticket
            var ticket = GetAuthenticationTicket();

            await this._taskManager.Remove(ticket, taskInstanceId).ConfigureAwait(false);

            this._eventLog.WriteEntry("Delete task - CPU " + this._counter.NextValue());

            return this.Request.CreateResponse(HttpStatusCode.OK);
        }

        [Route("")]
        public async Task<HttpResponseMessage> Get(
            string taskInstanceId = null, 
            bool overallStatus = false, 
            bool getStatus = false, 
            bool getResult = false, 
            bool deleteOnGetResult = false)
        {
            // get auth ticket
            var ticket = GetAuthenticationTicket();

            // get overall task statuses for a user
            if (overallStatus)
            {
                List<LongRunningTaskInstanceStatus> taskInstanceStatusLst = _taskManager.GetOverallStatus(ticket);

                return this.Request.CreateResponse(HttpStatusCode.OK, taskInstanceStatusLst);
            }

            // get task status
            if (getStatus)
            {
                LongRunningTaskInstanceStatus taskInstanceStatus = _taskManager.GetStatus(ticket, taskInstanceId);

                return this.Request.CreateResponse(HttpStatusCode.OK, taskInstanceStatus);
            }

            // get task result
            if (getResult)
            {
                // get task result
                ILongRunningTaskResult taskRes = _taskManager.GetResult(ticket, taskInstanceId);

                // create response message from task result
                HttpResponseMessage resMsg = GetResponseFromTaskResult(taskRes);

                // remove task
                if (deleteOnGetResult)
                {
                    await this.Delete(taskInstanceId).ConfigureAwait(false);
                }

                return resMsg;
            }

            return this.Request.CreateResponse(HttpStatusCode.BadRequest);
        }

        /// <summary>
        /// POST.
        /// Add new task.
        /// POST data can be sent as part of multipart form data or as part of URI.
        /// If is upload then the upload can be done using:
        /// 1. Multipart form data
        ///     - multiple files added as part of multipart form data
        ///     - POST data passed as another part of multipart form data
        /// 2. Single Stream
        ///     - one file as part of content stream
        ///     - POST data passed by URI => the length of data is limited to the max size of URI
        /// </summary>
        /// <param name="data"> POST data. Is ignored when using multipart form data. </param>
        /// <returns>returns a http response</returns>
        [Route("")]
        public async Task<HttpResponseMessage> Post([FromUri] LongRunningTaskPostData data)
        {
            // get ticket
            AuthenticationTicket ticket = GetAuthenticationTicket();

            _taskManager.CheckMaxTasksPerUserReachedException(ticket.Identity.Name);

            if (Request.Content.IsMimeMultipartContent())
            {
                data = await this.PostMimeMultipart(ticket).ConfigureAwait(false);
            }
            else
            {
                data = await this.PostStream(ticket, data).ConfigureAwait(false);
            }

            // start task
            string taskInstanceId = _taskManager.Start(
                data.Id, 
                data.Title, 
                ticket, 
                data.ParamJson, 
                data.TaskType, 
                data.IsUpload, 
                data.IsDownload, 
                data.Order);

            return this.Request.CreateResponse(HttpStatusCode.OK, taskInstanceId);
        }

        [Route("")]
        public async Task<HttpResponseMessage> Post([FromUri] string oauth)
        {
            HttpResponseMessage response;

            var sec = new CustomAccessTokenFormat();
            var ticket = sec.Unprotect(oauth);
            if (!Request.Content.IsMimeMultipartContent())
            {
                return Request.CreateErrorResponse(HttpStatusCode.NotImplemented, "There is no way in ie9");
            }

            var data = await this.PostMimeMultipart(ticket).ConfigureAwait(false);
            string taskInstanceId = _taskManager.Start(
                data.Id, 
                data.Title, 
                ticket, 
                data.ParamJson, 
                data.TaskType, 
                data.IsUpload, 
                data.IsDownload, 
                data.Order);
            response = Request.CreateResponse(HttpStatusCode.OK, taskInstanceId);

            response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
            return response;
        }

        [Route("")]
        public async Task<HttpResponseMessage> Post([FromUri] bool isIe, [FromBody] LongRunningTaskPostData data)
        {
            if (!isIe)
            {
                return Request.CreateResponse(HttpStatusCode.Forbidden);
            }

            AuthenticationTicket ticket = GetAuthenticationTicket();

            _taskManager.CheckMaxTasksPerUserReachedException(ticket.Identity.Name);
            await this.PostStream(ticket, data).ConfigureAwait(false);

            // start task
            string taskInstanceId = _taskManager.Start(
                data.Id, 
                data.Title, 
                ticket, 
                data.ParamJson, 
                data.TaskType, 
                data.IsUpload, 
                data.IsDownload, 
                data.Order);

            return this.Request.CreateResponse(HttpStatusCode.OK, taskInstanceId);
        }

        private static string RemoveFileNameDecoration(string decoratedFileName)
        {
            int startDecoration = decoratedFileName.IndexOf('#');
            if (startDecoration != 0)
            {
                return decoratedFileName;
            }

            int endDecoration = decoratedFileName.IndexOf('#', startDecoration + 1);
            if (endDecoration == -1)
            {
                return decoratedFileName;
            }

            return decoratedFileName.Substring(endDecoration + 1);
        }

        /// <summary>
        /// Get authentication ticket for current request.
        /// </summary>
        /// <returns> Authentication ticket. </returns>
        private AuthenticationTicket GetAuthenticationTicket()
        {
            AuthenticationTicket ticket = new AuthenticationTicket(
                (ClaimsIdentity)this.User.Identity, 
                new AuthenticationProperties());
            return ticket;
        }

        /// <summary>
        /// Get HttpResponseMessage content from task result.
        /// </summary>
        /// <param name="taskResult"> Task result. </param>
        /// <returns> returns a Http ResponseMessage. </returns>
        private HttpResponseMessage GetResponseFromTaskResult(ILongRunningTaskResult taskResult)
        {
                if (taskResult is LongRunningTaskObjectResult)
                {
                    var objRes = (LongRunningTaskObjectResult)taskResult;

                    return this.Request.CreateResponse(HttpStatusCode.OK, objRes.Data);
                }

                Func<ILongRunningTaskResult, LRTMFileInfo> LRTMFileInfoFactory=null;

            if ((taskResult as LongRunningTaskS3FileResult) != null)
            {
                LRTMFileInfoFactory = CreateFileInfoFromS3Result;
            }

            if ((taskResult as LongRunningTaskFileManagerResult) != null)
            {
                LRTMFileInfoFactory = GetFileInfoFromFileManager;
            }

            if ((taskResult as LongRunningTaskFileInMemResult) != null)
            {
               LRTMFileInfoFactory = GetFileInfoFromMemory;
            }

            if ((taskResult as LongRunningTaskFileOnDiskResult) != null)
            {
                LRTMFileInfoFactory = GetFileInfoFromDiskFile;
            }

            if (LRTMFileInfoFactory == null)
            {
                return null;
            }
                LRTMFileInfo fileInfo = LRTMFileInfoFactory(taskResult);
           

            return fileInfo.BuildHttpResponseMessage();
        }

        private LRTMFileInfo CreateFileInfoFromS3Result(ILongRunningTaskResult taskResult)
        {
            var s3Client =
                new S3Client(
                    new S3ClientSettings
                        {
                            AccessKey = SettingsHelper.Read("accesKeyId", "AKIAJKGZZMOFAVOVLHJA"),
                            SecretAccessKey =
                                SettingsHelper.Read(
                                    "awsSecretKey",
                                    "7aB5pz0c1ahfIOCdUfEOah08bvU04syMxi05cfmq"),
                            ServiceEndPoint =
                                SettingsHelper.Read(
                                    "s3ServiceEndPoint",
                                    "https://s3-eu-west-1.amazonaws.com/")
                        });

            var s3FileResult = taskResult as LongRunningTaskS3FileResult;

            LRTMFileInfo fileInfo = new LRTMFileInfo
            {
                StreamAccessor = () => s3Client.GetFileStream(s3FileResult.Bucket, s3FileResult.Key),
                FileName = s3FileResult.FileName,
                MimeType = s3FileResult.MimeType
            };
            return fileInfo;
        }

        private LRTMFileInfo GetFileInfoFromFileManager(ILongRunningTaskResult taskResult)
        {
            var fileMgrRes = taskResult as LongRunningTaskFileManagerResult;
            if (fileMgrRes==null) return null;
            FileContents fileContents = this._fileManager.GetFile(fileMgrRes.FilePath);
            LRTMFileInfo fileInfo = new LRTMFileInfo
                                        {
                                            // get file contents from FileManager
                                           

                                            StreamAccessor = fileContents.StreamAccessor,
                                            FileName =
                                                RemoveFileNameDecoration(fileContents.Metadata.FileName),
                                            MimeType = fileContents.Metadata.MimeType
                                        };
            return fileInfo;
        }

        private LRTMFileInfo GetFileInfoFromDiskFile(ILongRunningTaskResult taskResult)
        {
            var fileOnDiskRes = taskResult as LongRunningTaskFileOnDiskResult;
            if (fileOnDiskRes == null) return null;
            return new LRTMFileInfo
                                        {
                                            StreamAccessor = () => File.OpenRead(fileOnDiskRes.FilePath),
                                            FileName = fileOnDiskRes.FileName,
                                            MimeType = fileOnDiskRes.MimeType,
                                        };

        }

        private LRTMFileInfo GetFileInfoFromMemory(ILongRunningTaskResult taskResult)
        {
            var fileInMemRes = taskResult as LongRunningTaskFileInMemResult;
            if (fileInMemRes == null)
            {
                return null;
            }
            return new LRTMFileInfo
                                        {
                                            StreamAccessor =
                                                () => new MemoryStream(fileInMemRes.Contents),
                                            FileName = fileInMemRes.FileName,
                                            MimeType = fileInMemRes.MimeType
                                        };
        }

        private async Task<LongRunningTaskPostData> PostMimeMultipart(AuthenticationTicket ticket)
        {
            // ensure that the content exists
            if (Request.Content == null)
            {
                throw new LongRunningTaskUploadContentNullException();
            }

            // ensure upload folder exists
            if (Directory.Exists(this._config.UploadTemporaryDirectory) == false)
            {
                Directory.CreateDirectory(this._config.UploadTemporaryDirectory);
            }

            // --- Is upload then add upload content to file manager and then pass a reference to task manager ---

            // TO DO: create custom provider to upload directly to FileManager 
            // http://stackoverflow.com/questions/17072767/web-api-how-to-access-multipart-form-values-when-using-multipartmemorystreampro/17073113#17073113
            var provider = new MultipartFormDataStreamProvider(this._config.UploadTemporaryDirectory);

            // Read the form data.
            await this.Request.Content.ReadAsMultipartAsync(provider).ConfigureAwait(false);

            // get POST data
            string dataStr = provider.FormData["data"];
            if (string.IsNullOrEmpty(dataStr))
            {
                throw new PostTaskDataInvalidException();
            }

            // deserialize POST data
            LongRunningTaskPostData data = JsonConvert.DeserializeObject<LongRunningTaskPostData>(dataStr);
            if (data == null)
            {
                throw new PostTaskDataInvalidException();
            }

            // if request is no upload then return the deserialized POST data
            if (data.IsUpload == false)
            {
                return data;
            }

            List<string> filePaths = new List<string>();

            // foreach file in posted multipart request add the file to file manager
            Parallel.ForEach(
                provider.FileData, 
                file =>
                    {
                        // get file name
                        string fileName = file.Headers.ContentDisposition.FileName;

                        // create file path
                        string filePath = string.Concat(
                            ticket.Identity.Name, 
                            "/", 
                            fileName.Replace(@"\", string.Empty).Replace("\"", string.Empty));

                        // file located on disk
                        string diskFilePath = file.LocalFileName;

                        // add file to file manager
                        _fileManager.AddFile(
                            new FileContents(
                                filePath: filePath, 
                                metadata:
                                    new FileMetadata(
                                        fileName: fileName, 
                                        mimeType:
                                            file.Headers.ContentType == null ? null : file.Headers.ContentType.MediaType, 
                                        dateCreated: DateTime.UtcNow), 
                                streamAccessor: () => File.OpenRead(diskFilePath)));

                        filePaths.Add(filePath);
                    });

            // change params to include also the uploaded file path
            FileManagerFileTaskParam fileMgrTaskParam = new FileManagerFileTaskParam
                                                            {
                                                                FileManagerFilePaths =
                                                                    filePaths.ToArray()
                                                            };

            data.ParamJson = fileMgrTaskParam.AddToParamsJson(data.ParamJson);

            return data;
        }

        private async Task<LongRunningTaskPostData> PostStream(
            AuthenticationTicket ticket, 
            LongRunningTaskPostData data)
        {
            // ensure that POST data is not null
            if (data == null)
            {
                throw new PostTaskDataInvalidException();
            }

            // if is upload then add upload content to file manager and then pass a reference to task manager
            if (data.IsUpload == false)
            {
                return data;
            }

            // --- Is upload then add upload content to file manager and then pass a reference to task manager ---

            // ensure that there is a content for sending the file
            if (Request.Content == null)
            {
                throw new LongRunningTaskUploadContentNullException();
            }

            if (string.IsNullOrEmpty(data.FileName))
            {
                throw new UploadFileNameNullOrEmptyException();
            }

            // create file path
            string filePath = string.Concat(ticket.Identity.Name, "/", data.FileName);

            // get request stream
            Stream reqStream = await this.Request.Content.ReadAsStreamAsync().ConfigureAwait(false);

            // create file manager file contents
            var fileContents = new FileContents(
                filePath: filePath, 
                metadata:
                    new FileMetadata(fileName: data.FileName, mimeType: data.MimeType, dateCreated: DateTime.UtcNow), 
                streamAccessor: () => reqStream);

            // add file to file manager
            this._fileManager.AddFile(fileContents);

            // change params to include also the uploaded file path
            FileManagerFileTaskParam fileMgrTaskParam = new FileManagerFileTaskParam
                                                            {
                                                                FileManagerFilePaths =
                                                                    new[] { filePath }
                                                            };

            data.ParamJson = fileMgrTaskParam.AddToParamsJson(data.ParamJson);

            return data;
        }


        private class LRTMFileInfo
        {
            public LRTMFileInfo()
            {
                this.FileName = null;
                this.MimeType = null;
                this.StreamAccessor = null;
            }

            public string FileName { get; set; }

            public string MimeType { get; set; }

            public Func<Stream> StreamAccessor { get; set; }

            public HttpResponseMessage BuildHttpResponseMessage()
            {
                if (this.StreamAccessor == null)
                {
                    return null;
                }

                var resMsg = new HttpResponseMessage(HttpStatusCode.OK);

                // send file
                var streamContent = new PushStreamContent(
                    (outStream, httpContent, context) =>
                    {
                        byte[] bytes = new byte[BufferSize];

                        using (Stream srcStream = this.StreamAccessor())
                        {
                            int bufferCount;
                            while ((bufferCount = srcStream.Read(bytes, 0, bytes.Length)) > 0)
                            {
                                outStream.Write(bytes, 0, bufferCount);
                                outStream.Flush();
                            }
                        }

                        outStream.Close();
                    });

                streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachement")
                {
                    FileName = this.FileName
                };

                streamContent.Headers.ContentType = new MediaTypeHeaderValue(this.MimeType ?? "application/octet-stream");

                resMsg.Content = streamContent;

                return resMsg;
            }
        }
    }
}